package com.itheima.config;
/*Spring Security的配置类，用于配置Web应用程序的安全特性。
Spring Security是一个强大的框架，用于为基于Spring的应用程序提供身份验证和授权。*/
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.config.annotation.authentication.builders.*;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.*;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.web.access.AccessDeniedHandler;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import org.springframework.security.web.savedrequest.HttpSessionRequestCache;
import org.springframework.security.web.savedrequest.RequestCache;
import org.springframework.security.web.savedrequest.SavedRequest;

import javax.servlet.RequestDispatcher;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.sql.DataSource;
import java.io.IOException;
import java.net.URL;
import java.util.Collection;

@EnableWebSecurity  /* 开启MVC security安全支持. 这是一个Spring Security的注解，用于启用Web安全支持。
当它被应用到一个配置类时，它会自动为Spring应用程序配置安全相关的Bean。*/
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    /*WebSecurityConfigurerAdapter是一个方便的类，
    它提供了许多默认的安全配置，并允许你通过重写其方法来定制这些配置。
     */
    @Autowired
    /*  举一反三：：：：@Autowired: 这是一个Spring的注解，用于自动装配依赖项。
    在这里，它用于自动装配DataSource对象。
     */
    private DataSource dataSource;
    /*private DataSource dataSource;: 这是一个DataSource类型的字段，用于连接数据库。
    由于它被@Autowired注解，Spring会自动为它注入一个合适的DataSource Bean。
     */
    @Value("${COOKIE.VALIDITY}")
    /*@Value("${COOKIE.VALIDITY}"): 这是一个Spring的注解，用于从配置文件中注入属性值。
    在这里，它试图从配置文件中获取名为COOKIE.VALIDITY的属性值，并将其注入到COOKIE_VALIDITY字段中。*/
    private Integer COOKIE_VALIDITY;
    /*private Integer COOKIE_VALIDITY;: 这是一个整数类型的字段，
    用于存储从配置文件中获取的cookie有效期。
     */

    /**
     * 重写configure(HttpSecurity http)方法，进行用户授权管理
     * @param http
     * @throws Exception
     */
    @Override
    /*Spring Security配置中的一部分，它重写了WebSecurityConfigurerAdapter中的configure(HttpSecurity http)方法，
    用于配置Web应用的安全设置。
     */
    protected void configure(HttpSecurity http) throws Exception {
        // 1、自定义用户访问控制
        http.authorizeRequests()
                //使用authorizeRequests()方法开始配置授权请求
                .antMatchers("/","/page/**","/article/**","/login").permitAll()//在这些路径下都允许所有用户访问，无需任何权限。
                .antMatchers("/back/**","/assets/**","/user/**","/article_img/**").permitAll()//在这些路径下都允许所有用户访问，无需任何权限。
                .antMatchers("/admin/**").hasRole("admin")//“admin”角色的用户才能访问。
                .anyRequest().authenticated();// 表示对于其他所有请求，用户都必须进行身份验证。
        // 2、自定义用户登录控制
        http.formLogin()//使用formLogin()方法开始配置基于表单的登录。
                .loginPage("/login")//设置登录页面为/login。
                .usernameParameter("username").passwordParameter("password")
                //指定登录表单中用户名和密码的字段名分别为“username”和“password”。
                .successHandler(new AuthenticationSuccessHandler() {// 自定义登录成功后的处理逻辑。
                    @Override
                    public void onAuthenticationSuccess(HttpServletRequest httpServletRequest,HttpServletResponse httpServletResponse, Authentication authentication) throws IOException, ServletException {
                        String url = httpServletRequest.getParameter("url");
                        //在这个successHandler中，首先尝试从请求中获取一个名为“url”的参数，这可能是用户尝试访问但被拦截的原始URL。
                        // 获取被拦截的原始访问路径
                        RequestCache requestCache = new HttpSessionRequestCache();
                        SavedRequest savedRequest = requestCache.getRequest(httpServletRequest,httpServletResponse);
                        if(savedRequest !=null){
                            // 如果存在原始拦截路径，登录成功后重定向到原始访问路径
                            httpServletResponse.sendRedirect(savedRequest.getRedirectUrl());
                        } else if(url != null && !url.equals("")){
                            // 跳转到之前所在页面
                            URL fullURL = new URL(url);
                            httpServletResponse.sendRedirect(fullURL.getPath());
                        }else {
                            // 直接登录的用户，根据用户角色分别重定向到后台首页和前台首页
                            Collection<? extends GrantedAuthority> authorities = authentication.getAuthorities();
                            boolean isAdmin = authorities.contains(new SimpleGrantedAuthority("ROLE_admin"));
                            if(isAdmin){
                                httpServletResponse.sendRedirect("/admin");
                            }else {
                                httpServletResponse.sendRedirect("/");
                            }
                        }
                    }
                })
                // 用户登录失败处理
                .failureHandler(new AuthenticationFailureHandler() {/*在.failureHandler中，你需要实现登录失败后的具体处理逻辑。
                    这通常包括重定向到登录页面、设置错误消息或进行其他操作。*/
                    @Override
                    public void onAuthenticationFailure(HttpServletRequest httpServletRequest,HttpServletResponse httpServletResponse, AuthenticationException e) throws IOException, ServletException {
                        // 登录失败后，取出原始页面url并追加在重定向路径上
                        String url = httpServletRequest.getParameter("url");
                        httpServletResponse.sendRedirect("/login?error&url="+url);
                    }
                });
        // 3、设置用户登录后cookie有效期，默认值
        //alwaysRemember(true) 表示用户登录后总是创建一个“记住我”的cookie。
        //tokenValiditySeconds(COOKIE_VALIDITY) 指定了“记住我”cookie的有效期。
        http.rememberMe().alwaysRemember(true).tokenValiditySeconds(COOKIE_VALIDITY);
        // 4、自定义用户退出控制
        //logoutUrl("/logout") 指定了退出登录的URL为 /logout。
        //logoutSuccessUrl("/") 指定了用户成功退出后重定向到的URL。
        http.logout().logoutUrl("/logout").logoutSuccessUrl("/");

        // 5、针对访问无权限页面出现的403页面进行定制处理
        //这里定义了一个自定义的AccessDeniedHandler，当访问无权限页面时，
        // Spring Security会抛出AccessDeniedException异常，这个异常会被此处理器捕获。

        //处理器通过RequestDispatcher将请求转发到指定的错误页面/errorPage/comm/error_403。
        http.exceptionHandling().accessDeniedHandler(new AccessDeniedHandler() {
            @Override
            public void handle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AccessDeniedException e) throws IOException, ServletException {
                // 如果是权限访问异常，则进行拦截到指定错误页面
                RequestDispatcher dispatcher = httpServletRequest.getRequestDispatcher("/errorPage/comm/error_403");
                dispatcher.forward(httpServletRequest, httpServletResponse);
            }
        });
    }

    /**
     * 重写configure(AuthenticationManagerBuilder auth)方法，进行自定义用户认证
     * @param auth
     * @throws Exception
     */
    //这里使用了BCryptPasswordEncoder作为密码编码器，它会对密码进行哈希处理。
    //通过jdbcAuthentication()方法配置了基于JDBC的用户认证。
    //usersByUsernameQuery(userSQL)指定了从数据库中查询用户信息的SQL语句。
    //authoritiesByUsernameQuery(authoritySQL)指定了从数据库中查询用户权限信息的SQL语句。这里使用了表连接来查询用户的角色/权限。
    //dataSource(dataSource)指定了数据源的Bean，Spring Security会使用这个数据源来执行上述的SQL查询。
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        //  密码需要设置编码器
        BCryptPasswordEncoder encoder = new BCryptPasswordEncoder();
        //  使用JDBC进行身份认证
        String userSQL ="select username,password,valid from t_user where username = ?";
        String authoritySQL ="select u.username,a.authority from t_user u,t_authority a," +
                             "t_user_authority ua where ua.user_id=u.id " +
                             "and ua.authority_id=a.id and u.username =?";
        auth.jdbcAuthentication().passwordEncoder(encoder)
                .dataSource(dataSource)
                .usersByUsernameQuery(userSQL)
                .authoritiesByUsernameQuery(authoritySQL);
    }
}

